﻿using System;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Windows.Input;
using MouseExtender.Logic.Converters;
using MouseExtender.Logic.Enums;
using MouseExtender.Logic.Helpers;

namespace MouseExtender.Logic.Common
{
    public class WindowsHook : CriticalFinalizerObject
    {
        private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

        #region Fields

        private IntPtr _hMouseHook = IntPtr.Zero;
        private MouseExtenderButton _hookButtons = MouseExtenderButton.MiddleButton;
        private static HookProc MouseHookProcedure;

        #endregion

        #region Constructors

        public WindowsHook(MouseExtenderButton hookButtons)
        {
            _hookButtons = hookButtons;

            if ((_hookButtons & MouseExtenderButton.LeftButton) == MouseExtenderButton.LeftButton)
                HookButtonConstant = WM_LBUTTONDOWN;
            else if ((_hookButtons & MouseExtenderButton.RightButton) == MouseExtenderButton.RightButton)
                HookButtonConstant = WM_RBUTTONDOWN;
            else if ((_hookButtons & MouseExtenderButton.MiddleButton) == MouseExtenderButton.MiddleButton)
                HookButtonConstant = WM_MBUTTONDOWN;
            else if (((_hookButtons & MouseExtenderButton.XButton1) == MouseExtenderButton.XButton1) ||
            ((_hookButtons & MouseExtenderButton.XButton2) == MouseExtenderButton.XButton2))
                HookButtonConstant = WM_XBUTTONDOWN;
            else
                HookButtonConstant = WM_MBUTTONDOWN;

            HookKeys = MouseExtenderButtonHelper.ConvertToModifierKeys(_hookButtons);

            Start();
        }

        #endregion

        ~WindowsHook()
        {
            Stop();
        }

        #region Windows structure definition

        [StructLayout(LayoutKind.Sequential)]
        public struct POINT
        {
            public int x;
            public int y;
        }

        [StructLayout(LayoutKind.Sequential)]
        public class MouseHookStruct
        {
            public POINT pt;
            public uint mouseData;
            public uint flags;
            public uint time;
            public IntPtr dwExtraInfo;
        }

        #endregion

        #region Windows constants

        private const int WH_MOUSE_LL = 0x0E;

        private const int WM_LBUTTONDOWN = 0x0201;
        private const int WM_LBUTTONUP = 0x0202;
        private const int WM_RBUTTONDOWN = 0x0204;
        private const int WM_RBUTTONUP = 0x0205;
        private const int WM_MBUTTONDOWN = 0x0207;
        private const int WM_MBUTTONUP = 0x0208;
        private const int WM_XBUTTONDOWN = 0x020B;
        private const int WM_XBUTTONUP = 0x020C;
        private const int WM_MOUSEWHEEL = 0x020A;

        private const uint WM_MOUSEWHEEL_UP = 0x780000;
        private const uint WM_MOUSEWHEEL_DOWN = 0xFF880000;

        #endregion

        #region Windows fonction imports

        // CallingConvention = CallingConvention.StdCall
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UnhookWindowsHookEx(IntPtr idHook);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, IntPtr wParam, IntPtr lParam);

        #endregion

        #region Properties

        private int HookButtonConstant
        {
            get;
            set;
        }

        private ModifierKeys HookKeys
        {
            get;
            set;
        }

        #endregion

        private bool CheckButtonsConfiguration(MouseHookStruct mouseHookStruct)
        {
            ModifierKeys modifiers = Win32Helper.GetModifierKeys();

            if (modifiers != HookKeys)
                return false;

            if (HookButtonConstant == WM_XBUTTONDOWN)
            {
                int xbutton = (int)mouseHookStruct.mouseData >> 16;
                bool xButton1 = (xbutton == 1 && ((_hookButtons & MouseExtenderButton.XButton1) == MouseExtenderButton.XButton1));
                bool xButton2 = (xbutton == 2 && ((_hookButtons & MouseExtenderButton.XButton2) == MouseExtenderButton.XButton2));
                if (!xButton1 && !xButton2)
                    return false;
            }
            else if (HookButtonConstant == WM_MBUTTONDOWN)
            {
                if (mouseHookStruct.dwExtraInfo.ToInt32() != 0)
                    return false;
            }

            return true;
        }

        private IntPtr MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0)
            {
                if (MouseHookEvent != null && (int)wParam == HookButtonConstant)
                {
                    PreviewMouseHookCancelArgs previewArgs = new PreviewMouseHookCancelArgs();
                    PreviewMouseHookEvent(this, previewArgs);
                    if (!previewArgs.Cancel)
                    {
                        MouseHookStruct mouseHookStruct = (MouseHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseHookStruct));
                        bool buttonsOk = CheckButtonsConfiguration(mouseHookStruct);

                        if (buttonsOk)
                        {
                            MouseButton button = MouseExtenderButtonHelper.ConvertToMouseButton(_hookButtons);
                            MouseHookEventArgs e = new MouseHookEventArgs(button, mouseHookStruct.pt.x, mouseHookStruct.pt.y, previewArgs.ProcessExecutableName);
                            MouseHookEvent(this, e);
                            return (IntPtr)1;
                        }
                    }
                }

                if (MouseWheelEvent != null && (int)wParam == WM_MOUSEWHEEL)
                {
                    PreviewMouseHookCancelArgs previewArgs = new PreviewMouseHookCancelArgs();
                    PreviewMouseWheelHookEvent(this, previewArgs);
                    if (!previewArgs.Cancel)
                    {
                        MouseHookStruct mouseHookStruct = (MouseHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseHookStruct));
                        short delta = 0;
                        if (mouseHookStruct.mouseData == WM_MOUSEWHEEL_UP)
                            delta = -1;
                        else if (mouseHookStruct.mouseData == WM_MOUSEWHEEL_DOWN)
                            delta = 1;
                        MouseHookEventArgs e = new MouseHookEventArgs(MouseButton.Middle, mouseHookStruct.pt.x, mouseHookStruct.pt.y, 0, delta, String.Empty);
                        MouseWheelEvent.Invoke(this, e);
                    }
                }
            }

            return CallNextHookEx(_hMouseHook, nCode, wParam, lParam);
        }

        public void Start()
        {
            if (_hMouseHook == IntPtr.Zero)
            {
                MouseHookProcedure = new HookProc(MouseHookProc);
                IntPtr hMod = Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]);
                _hMouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProcedure, hMod, 0);

                if (_hMouseHook == IntPtr.Zero)
                {
                    int errorCode = Marshal.GetLastWin32Error();
                    Stop();
                    throw new Win32Exception(errorCode);
                }
            }
        }

        public void Stop()
        {
            if (_hMouseHook != IntPtr.Zero)
            {
                bool retMouse = UnhookWindowsHookEx(_hMouseHook);
                _hMouseHook = IntPtr.Zero;
                MouseHookProcedure = null;
                if (!retMouse)
                {
                    int errorCode = Marshal.GetLastWin32Error();
                    throw new Win32Exception(errorCode);
                }
            }
        }

        public event EventHandler<PreviewMouseHookCancelArgs> PreviewMouseHookEvent = (s, e) => { };
        public event EventHandler<PreviewMouseHookCancelArgs> PreviewMouseWheelHookEvent = (s, e) => { };
        public event MouseHookHandler MouseHookEvent = (s, e) => { };
        public event MouseHookHandler MouseWheelEvent = (s, e) => { };
    }
}
